Skip to content

Detect property to exclude Cargo dependencies #1421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

zahidblackduck
Copy link
Collaborator

JIRA Ticket

IDETECT-4326

Description

This merge request introduces a feature to exclude specific types of dependencies in the Cargo detector, providing more control over which dependencies are included in detect scans. The supported types are:

  • Normal: Regular dependencies needed for runtime.
  • Dev: Development dependencies, such as test libraries.
  • Build: Build-related dependencies.
  • Proc-Macro: Procedural macro dependencies.

A new property has been added to configure the exclusion of these dependencies:

--detect.cargo.dependency.types.excluded=NONE,NORMAL,DEV,BUILD,PROC_MACRO

By default, all dependencies are included (NONE). Specific types can be excluded by specifying the appropriate values.

Implementation Details

To support this feature, an enum (CargoDependencyType) has been introduced to represent the dependency types. This logic has been implemented across both Cargo CLI Detector and Cargo Lockfile Detector:

  1. Cargo CLI Detector: Uses the cargo tree command with native options to exclude dependencies. For example, excluding development dependencies is done by adding the --edges no-dev flag to the command. This solution leverages the CLI's built-in functionality with minimal post-processing.

  2. Cargo Lockfile Detector: Since Cargo.lock does not include dependency types, the lockfile detector reads and parses the Cargo.toml file. It collects the [dev-dependencies] and [build-dependencies] sections, filtering out matching dependencies based on the exclusion configuration.

@zahidblackduck zahidblackduck marked this pull request as draft May 6, 2025 13:43
@zahidblackduck zahidblackduck self-assigned this May 6, 2025
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another wildcard.

List<CargoLockPackageData> filteredPackages = cargoLockPackageDataList;
String cargoTomlContents = FileUtils.readFileToString(cargoTomlFile, StandardCharsets.UTF_8);

if (cargoDetectableOptions != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking on cargoDetectableOptions object, can we check on dependencyTypeFilter instead? There could be more properties added to this detector in future not related to dependency exclusion.

Copy link
Collaborator Author

@zahidblackduck zahidblackduck May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right. The actual filtering is handled by dependencyTypeFilter. This null check is just to verify whether any options were passed at all. The filtering logic using dependencyTypeFilter is applied later in the CargoTomlParser#parseDependenciesToExclude(..) method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that, my only concern is that if in future lets say we add some new property for excluding workspaces as an example then those options will also be passed in CargoDetectableOptions, so we will make this unnecessary call to cargoTomlParser which is not required if no dependency exclusion is involved. I hope I made sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code setup doesn't call the methods of CargoTomlParser if no dependency exclusion is involved. Nonetheless, I aim to address the issue that you pointed out in any future enhancement of cargo related issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, currently your code is not getting executed if cargoDetectableOptions is null. It is not guaranteed that you are going to work on this in any future enhancement related to Cargo and it could be easily missed. I believe we should address this now, its a simple change and not a complicated ask.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sure. I'll update accordingly.

.setInfo("Cargo Dependency Types Excluded", DetectPropertyFromVersion.VERSION_10_5_0)
.setHelp(
"A comma-separated list of dependency types that will be excluded.",
"If DEV is excluded, the Cargo CLI Detector will exclude 'dev' dependencies when parsing the output of cargo tree."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed, since this is applicable to Lockfile Detector as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good find. I'll update.

@@ -1,16 +1,22 @@
package com.blackduck.integration.detectable.detectables.cargo.parse;

import java.util.Optional;
import java.util.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another wildcard import statement.

.setInfo("Cargo Dependency Types Excluded", DetectPropertyFromVersion.VERSION_10_5_0)
.setHelp(
"A comma-separated list of dependency types that will be excluded.",
"The Cargo CLI Detector uses cargo tree flags to exclude the specified types, while the Cargo Lockfile Detector filters dependencies by reading Cargo.toml. For example, passing DEV will skip [dev-dependencies] from detection."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be inclined to pass the full parameter entry for the example?
i.e. "For example, passing DETECT_CARGO_DEPENDENCY_TYPES_EXCLUDED = DEV will skip [dev-dependencies] from detection."

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Updated as per the review.

@andrian-sevastyanov
Copy link
Contributor

For lock file detectable, have you tested a scenario like this:

  • normal components: componentA
  • dev components: componentB
  • transitives: componentA dependends on componentB

I believe when excluding dev dependencies in such a case both componentA and componentB should be present in the result; however, in the case of lock file detectable we will exclude componentB.

Please correct me if I'm wrong.

.allMatch(dependencyTypeFilter::shouldExclude);

if (shouldBeExcluded) {
dependenciesToExclude.put(nameVersion.getName(), nameVersion.getVersion());
Copy link
Contributor

@andrian-sevastyanov andrian-sevastyanov May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dependenciesToExclude map allows for a maximum of one version per dependency name; however, it looks like it's possible to specify incompatible versions of the same dependency if they are different types. Example excerpt from Cargo.toml:

[dev-dependencies]
syn = "2.0.1"

[build-dependencies]
syn = "1"

results in the following tree:

[build-dependencies]
└── syn v1.0.109
    ├── proc-macro2 v1.0.95
    │   └── unicode-ident v1.0.18
    ├── quote v1.0.40
    │   └── proc-macro2 v1.0.95 (*)
    └── unicode-ident v1.0.18
[dev-dependencies]
└── syn v2.0.101
    ├── proc-macro2 v1.0.95 (*)
    ├── quote v1.0.40 (*)
    └── unicode-ident v1.0.18

and Cargo.lock has:

...
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]
...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a map of type Map<NameVersion, String> instead of Map<String, String> might solve this problem, as NameVersion key might be unique for a Cargo.toml file.

@andrian-sevastyanov
Copy link
Contributor

I have an idea that could help resolve the special cases we have been discussing. For Cargo.lock, when excluding dependencies we could try something like this:

  1. Go through sections of Cargo.toml to determine which sections should be included
  2. For all included sections, figure out what the root dependencies are
  3. Use the lock file to figure out the transitive dependencies for root dependencies from step 2
  4. The final result is then combination of step 2 and 3.

I may have missed something, but I think it's worth considering at the very least.

.map(cargoLockPackageDataTransformer::transform)
.collect(Collectors.toList());

DependencyGraph graph = cargoLockPackageTransformer.transformToGraph(packages);

Optional<NameVersion> projectNameVersion = Optional.empty();
if (cargoTomlFile != null) {
String cargoTomlContents = FileUtils.readFileToString(cargoTomlFile, StandardCharsets.UTF_8);
Copy link
Contributor

@andrian-sevastyanov andrian-sevastyanov May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we were checking whether the file is not null before reading it. Now, we don't. FileUtils.readFileToString throws an exception.
We should continue to check whether the file is there before reading it.

Also, I think something like this should happen:

if cargoTomlFile == null and isDependencyExclusionEnabled():
    failExtraction() // because we can't reliably determine dependency types; also, we might want to do this in the `extractable()` method of Detectable

ExternalId childExternalId = externalIdFactory.createNameVersionExternalId(Forge.CRATES, childName, childVersion);

graph.setDependencyInfo(childId, childName, childVersion, childExternalId);
graph.setDependencyAsAlias(childId, LazyId.fromName(childName));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are aliases used for? I've seen this before but not sure what the end effect is.

exclusionMap.put(CargoDependencyType.NORMAL, "no-normal");
exclusionMap.put(CargoDependencyType.BUILD, "no-build");
exclusionMap.put(CargoDependencyType.DEV, "no-dev");
exclusionMap.put(CargoDependencyType.PROC_MACRO, "no-proc-macro");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you describe situations in which PROC_MACRO exclusion is useful? And I guess it's not applicable to lock file extractions?

@zahidblackduck
Copy link
Collaborator Author

I have an idea that could help resolve the special cases we have been discussing. For Cargo.lock, when excluding dependencies we could try something like this:

  1. Go through sections of Cargo.toml to determine which sections should be included
  2. For all included sections, figure out what the root dependencies are
  3. Use the lock file to figure out the transitive dependencies for root dependencies from step 2
  4. The final result is then combination of step 2 and 3.

I may have missed something, but I think it's worth considering at the very least.

Interesting idea. Further discussion might help on the implementation strategy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants